/*
 * Copyright (C) 2014 Tolstykh Vladislav
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package lib;

import com.google.common.collect.TreeMultimap;
import java.util.Stack;
import lib.scripture.Chapter;
import lib.scripture.Content;
import lib.scripture.HashableBook;
import lib.scripture.Verse;
import lib.search.Description;
import lib.search.Handler;
import lib.search.engine.Haystack;
import lib.search.engine.Needle;
import lib.sort.Sort;
import lib.sort.Sortable;

/**
 * Тексты Библии.
 * @author Толстых Владислав
 */
public class BibleLib{
    private final Thread[] THR_ARR;
    private boolean done =false;
    private Stack<HashableBook> lifo; 
    
    private TreeMultimap<Description, Verse> response;
    /**
     * Латинские названия книг Библии.
     */
    public static final String[] LATIN = new String[]{
        "Genesis","Exodus","Leviticus","Numeri","Deuteronomium","Iosue",
        "Iudicum","Ruth","Samuelis1","Samuelis2","Regum1","Regum2",
        "Paralipomenon1","Paralipomenon2","Esdrae","Nehemiae","Esther","Iob",
        "Psalmorum","Proverbiorum","Ecclesiasticus","CanticumCanticorum",
        "Isaiae","Ieremiae","Lamentationes","Ezechielis","Danielis","Osee",
        "Ioel","Amos","Abdiae","Ionae","Michaeae","Nahum","Habacuc","Sophoniae",
        "Aggaei","Zachariae","Malachiae","Matthaeum","Marcum","Lucam","Ioannem",
        "Actus","Iacobi","Petri1","Petri2","Ioannis1","Ioannis2","Ioannis3",
        "Iudae","Romanos","Corinthios1","Corinthios2","Galatas","Ephesios",
        "Philippenses","Colossenses","Thessalonicenses1","Thessalonicenses2",
        "Timotheum1","Timotheum2","Titum","Philemonem","Hebraeos","Apocalypsis"
    };            
    /**
     * Язык, используемый при загрузке.
     */
    private final String LANG;
    /**
     * Массив объектов, представляющих Библию.
     */
    private HashableBook[] BOOKS;
    private final int THREADS_COUNT;
    
    /**
     * Создаём новый экземпляр библиотеки.
     * @param lang язык для загрузки, например "ru"
     * @param cores количество ядер процессора. 
     * Количество потоков для загрузки и работы библиотеки будет меньше на 1
     * @throws java.lang.InterruptedException работа booksInit() в потоках
     */
    public BibleLib(String lang, int cores) throws InterruptedException {
        LANG=lang;
        THREADS_COUNT = calculate(cores);
        THR_ARR = new Thread[THREADS_COUNT];        
        lifo=new Stack<>();        
    }

    /**
     * Получить текстовую ячейку Библии по иерархическому "адресу" - номер 
     * книги, глава, стих.
     * @param whereIs местоположение текста. Например: int[]{1,2,3}
     * @return стих целиком
     */
    public String textAt(int[] whereIs){        
        int bookId = whereIs[0]-1;
        int chaptId = whereIs[1];
        int verseId = whereIs[2];
        Content content = BOOKS[bookId].getContent();
        Chapter chapter = content.chaptAt(chaptId);
        Verse verse = chapter.verseAt(verseId);
        return verse.toString();
    }
    
    /**
     * Получить экземпляр HashableBook.
     * @param id порядковый номер, начиная с 0
     * @return книгу целиком
     */
    public HashableBook bookAt(int id){
        return BOOKS[id];
    }

    /**
     * Создаём модель Библии.
     * @throws InterruptedException работа по загрузке книг в четырёх потоках 
     */
    public void booksInit() throws InterruptedException {
        BOOKS = new HashableBook[66];
        //System.out.println("Загрузка книг...");             
        for (int i = 0; i < 66; i++) {
            final int id = i;
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    //System.out.println(LATIN[id]+".xml");
                    BOOKS[id] = new HashableBook(id,LANG);
                    //progress.put(id, BOOKS[id]);
                    lifo.push(BOOKS[id]);
                }
            };
            multiThreadsHandler(runnable, i, THREADS_COUNT);
        }
        finalThreadsJoin();
        done=true;
    }
    
    /**
     * Поиск текста.
     * @param expression образ для поиска
     * @param regex обработка регулярных выражений
     * @return 
     * @throws NullPointerException при отсутствии результатов поиска
     * @throws InterruptedException 
     */
    public TreeMultimap <Description, Verse> textFind(final String expression, final boolean regex) throws NullPointerException, InterruptedException{
        response = TreeMultimap.create(new Sortable(Sort.AS_IS), new Sortable(Sort.VERSE));
        for (int i = 0; i < BOOKS.length; i++) {
            final int id = i;
            Runnable searchForSingleBook = new Runnable() {
                @Override
                public void run() {
                    resultForSingleBook(BOOKS[id],new Needle(expression.toCharArray(), regex));
                }
            };            
            multiThreadsHandler(searchForSingleBook, i, THREADS_COUNT);
        }        
        finalThreadsJoin();
        return response;
    }
    
    /**
     * Распаралеллить вычисления на потоки.
     * @param runnable описание работы
     * @param cycleСounter счётчик цикла, для деления по модулю на количесво потоков
     * @param threadsCount оптимальное количество потоков
     * @throws InterruptedException 
     */
    private void multiThreadsHandler(Runnable runnable, int cycleСounter, int threadsCount) throws InterruptedException{
        int id = cycleСounter % threadsCount;
        if(THR_ARR[id] != null){
            do{                
                //Thread.sleep(1);
            }while(THR_ARR[id].isAlive());             
        }   
        THR_ARR[id] = new Thread(runnable);
        THR_ARR[id].start();   
    }
    
    /**
     * Ожидаем корректного завершения работы последних потоков.
     * Если для работы метода multiThreadsHandler() используем цикл, то вызывать 
     * этот метод нужно за границами цикла. Например: for(Object obj : objArr) 
     * { multiThreadsHandler() } finalThreadsJoin();
     * @throws InterruptedException 
     */
    private void finalThreadsJoin() throws InterruptedException{
        for (Thread thread : THR_ARR) {
            thread.join();            
        }

    }
    
    /**
     * Hовый поиск для выбранной книги.
     * @param book где будет поиск
     * @param needle что будем искать
     */
    private void resultForSingleBook(HashableBook book, Needle needle) {
        Handler handler = new Handler(book);
        Content сontent = handler.сontent();
        //работаем по главам
        while (сontent.hasMoreElements()) {
            //новый "стог сена" из следующей главы
            Haystack haystack = new Haystack(сontent.nextElement());
            //ищем иголку
            haystack.lookingForAneedle(needle);
            TreeMultimap<Chapter,Verse>  singleChapt = haystack.found();
            //делаем из результатов поиска новую тримэпу
            TreeMultimap<Description, Verse> multiChapt = handler.collect(singleChapt);
            //добавляем в общий результат, результаты данной главы
            response.putAll(multiChapt);
        }
    }
    
    /**
     * Пробуем "общаться" с загрузчиком.
     * @return верхний объект стека (Last In First Out)
     */
    public HashableBook getLifo(){
        return lifo.pop();
    }
    
    /**
     * Завершение загрузки.
     * @return 
     */
    public boolean done(){
        return done;
    }
    
    /**
     * 
     * @return 
     */
    public boolean stackEmpty(){
        return lifo.empty();
    }

    /**
     * Рассчитать количество потоков.
     * @param cores
     * @return 
     */
    private int calculate(int cores) {
        if(cores>1)
            return cores-1;
        return 1;
    }
}
